Skip to content

fix(treeview): typed TreeView<T> with viewBuilder; obsolete ContentElement (#447)#452

Closed
codemonkeychris wants to merge 2 commits into
mainfrom
fix/447-treeview-typed-viewbuilder
Closed

fix(treeview): typed TreeView<T> with viewBuilder; obsolete ContentElement (#447)#452
codemonkeychris wants to merge 2 commits into
mainfrom
fix/447-treeview-typed-viewbuilder

Conversation

@codemonkeychris
Copy link
Copy Markdown
Collaborator

Fixes #447.

Problem

A TreeView whose TreeViewNodeData nodes carry a ContentElement renders blank rows — a ListView given the same element content renders fine.

Verified empirically on native ARM64 (WinUITreeViewNativeProbe): WinUI's node-mode TreeView stringifies the node and cannot host a live UIElement placed in TreeViewNode.Content (the default ContentPresenter receives the node, not its content). Rich per-node visuals in WinUI come from a DataTemplate/ItemTemplate, never a pre-built element. (This is a WPF-ism — there TreeViewItem is a HeaderedContentControl.)

Variant (native probe) Result
node.Content = Button, no template ❌ blank (stringified)
node.Content = Button + ContentControl Content="{Binding Content}" ✅ hosted
node.Content = "string", no template ✅ text

Fix — typed TreeView<T>

A data-driven, hierarchical peer of ListView<T>:

TreeView<T>(items, keySelector, childrenSelector, viewBuilder)   // + IReactorKeyed overload
  • viewBuilder is the per-node template (WinUI ItemTemplate equivalent); childrenSelector gives hierarchy; keySelector gives identity.
  • OnItemInvoked / OnExpanding hand the developer's own T back (resolved via an attached property on the node — duplicate-RCW-safe like ReactorState).
  • Heterogeneous nodes + per-shape templates fall out of a switch in the viewBuilder (the ItemTemplateSelector pattern in C#).
  • Keyed, in-place hierarchical reconcile so descendant component state survives updates.

Hosting (WinUI's own mechanism)

Each node's view is mounted once and kept on an attached property; the internal TreeViewList's ContainerContentChanging hosts it on realize and releases it (Content = null) on recycle — the only correct way to keep a shared live element from blanking across collapse/expand under virtualization. The host is deferred to a low-priority dispatch and re-validates the container still belongs to the node, so it never perturbs the synchronous expand pass (which otherwise re-prepared the expanding node with a stale IsExpanded and snapped it shut). Expansion is uncontrolled — the selector is only pushed when its value changes, never clobbering user toggles.

Deprecation

TreeViewNodeData.ContentElement is [Obsolete] pointing to TreeView<T>; the legacy path stays functional (warning-suppressed) for back-compat. Text-only TreeViewNodeData is unchanged.

Samples

  • DataTemplateDemo §4 migrated to TreeView<T> (heterogeneous pet groups vs. animal leaves).
  • Gallery TreeViewPage gains a file-explorer card (folders / docs / images via per-shape templates) and switches source snippets to raw-string literals so they render multi-line.

Tests

TemplatedTreeViewFixtures: rich-content render, events resolve T, keyed update reconcile, expansion-not-clobbered, collapse/expand cycle, expand-collapsed-node, value-type T. Plus WinUITreeViewNativeProbe documenting native node-mode behavior. Full self-test suite green (1075 fixtures / 4445 assertions, ARM64).

🤖 Generated with Claude Code

…ement (#447)

WinUI node-mode TreeView stringifies the node and cannot host a live
UIElement in TreeViewNode.Content (verified empirically on native ARM64),
so per-node `TreeViewNodeData.ContentElement` always rendered blank.

Adds a typed, data-driven `TreeView<T>` (TemplatedTreeViewElement<T>) — the
hierarchical peer of ListView<T>: per-node `viewBuilder` (the ItemTemplate
equivalent), `childrenSelector` for hierarchy, `keySelector` for identity.
OnItemInvoked/OnExpanding hand the developer's own T back. Heterogeneous
nodes + per-shape templates fall out of a switch in the viewBuilder.

Hosting follows WinUI's own mechanism: each node's view is mounted once and
kept on an attached property; the internal TreeViewList's
ContainerContentChanging hosts it on realize and releases it (Content=null)
on recycle. The host is deferred to a low-priority dispatch and re-validates
the container still belongs to the node, so it never perturbs the
synchronous expand pass (which otherwise re-prepared the expanding node with
a stale IsExpanded and snapped it shut). Expansion is uncontrolled — the
selector is only pushed when its value changes, never clobbering user toggles.

`TreeViewNodeData.ContentElement` is marked [Obsolete] pointing to
TreeView<T>; the legacy path stays functional (warning-suppressed) for
back-compat.

Samples: DataTemplateDemo §4 migrated to TreeView<T> (heterogeneous pet
groups vs. animals); gallery TreeViewPage gains a file-explorer card
(folders/docs/images via per-shape templates) and switches source snippets
to raw-string literals so they render multi-line.

Tests: TemplatedTreeViewFixtures (render, events resolve T, keyed update,
expansion-not-clobbered, collapse/expand cycle, expand-collapsed-node,
value-type T) + WinUITreeViewNativeProbe documenting native node-mode
behavior. Full self-test suite green (1075 fixtures).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/Reactor/Core/Reconciler.Update.cs Fixed
… TreeView<T> hosting declarative

Two expand/collapse fixes:

1. Legacy TreeView (the "expand flashes open then shut" regression that
   repros on main): DiffTreeViewNodes reset each live node's IsExpanded to
   the static TreeViewNodeData value on every reconcile. When a reconcile
   fires after the user toggles a node — e.g. an OnExpanding lazy-load that
   updates state, or any unrelated state change while nodes are data-expanded
   — it snapped the node back to the data's state. Now expansion is treated as
   uncontrolled: the data value is only applied when it actually changes
   between renders, never clobbering the user's (or WinUI's native) toggle.

2. TreeView<T>: applied the same uncontrolled-expansion rule, and reverted the
   experimental ContainerContentChanging hosting back to the declarative
   {Binding Content} template. The imperative host/recycle handler perturbed
   WinUI's synchronous expand pass (manifested as expand opening then closing,
   and child-click collapsing the parent); the binding never perturbs native
   expand/collapse.

Tests: legacy + typed expansion-not-clobbered guards, collapse/expand cycle,
expand-collapsed-node, constrained-viewport expand. Full suite green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment on lines +4151 to +4153
foreach (var ln in liveNodes)
if (GetTreeNodeItem(ln) is { } it)
existing[o.GetKey(it)] = ln;
@codemonkeychris
Copy link
Copy Markdown
Collaborator Author

Superseded by #453 (follow-up agent). Abandoning this branch.

@codemonkeychris codemonkeychris deleted the fix/447-treeview-typed-viewbuilder branch May 29, 2026 20:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] TreeView does not render per-node ContentElement (ListView/GridView do) -

1 participant